package cn.caplike.demo.repository.annotation.processor;

import com.google.auto.service.AutoService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Description: 注解处理器.<br>
 * Details: 首先我们需要指定当前 Processor 能够处理的注解, 以及支持的源码级别.
 * 通过实现 Processor 接口的方法 {@code getSupportedAnnotationTypes} & {@code getSupportedSourceVersion}
 * 和 {@code @SupportedAnnotationTypes} & {@code @SupportedSourceVersion} 注解的方式都可以做到这一点.<br>
 * Attention: <b>支持通配符, 如: cn.caplike.demo.repository.annotation.processor.*</b>
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2020-03-30 10:27
 */
@Slf4j
@SupportedAnnotationTypes({"cn.caplike.demo.repository.annotation.processor.BuilderProperty"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
@SuppressWarnings("unused")
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    /**
     * Description: 对于每一个标注了目标注解的源文件, 编译器都会调用该方法.
     *
     * @param annotations 配置为需要被处理的注解
     * @param roundEnv    {@link RoundEnvironment} 携带了当前和之前处理阶段的环境 (上下文) 信息
     * @return 应该返回为 {@code true}: 当注解都被处理了 (<strong>这样就不会传递给下一个 Processor</strong>).
     * @author LiKe
     * @date 2020-03-30 10:34:33
     * @see AbstractProcessor#process(Set, RoundEnvironment)
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        annotations.forEach(theAnnotation -> {
            log.debug("需要被处理的注解: {}", theAnnotation.getSimpleName().toString());

            // 获取所有被标注了 @BuilderProperty 注解的元素.
            final Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(theAnnotation);

            // ~ 保证标注了 @BuilderProperty 注解的方法是 set 方法
            //   @BuilderProperty annotation's user could erroneously annotate methods that are not actually setters.
            //   The setter method name should start with set, and the method should receive a single argument.
            //   So let’s separate the wheat from the chaff.
            //   In the following code,
            //   we use the Collectors.partitioningBy() collector to split annotated methods into two collections:
            //   correctly annotated setters and other erroneously annotated methods
            final Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
                    Collectors.partitioningBy(
                            element ->
                                    // 这里我们使用 Element.asType() 方法以接收 TypeMirror 的实例, 后者让我们即便在源码处理阶段也能够获取到类型.
                                    // Here we use the Element.asType() method to receive an instance of the TypeMirror class which gives us some ability to introspect types even though we are only at the source processing stage.
                                    ((ExecutableType) element.asType()).getParameterTypes().size() == 1 && element.getSimpleName().toString().startsWith("set")
                    )
            );
            final List<Element> setters = annotatedMethods.get(true);
            final List<Element> otherMethods = annotatedMethods.get(false);
            otherMethods.forEach(element -> processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@BuilderProperty must be applied to a setXxx method " + "with a single argument", element));

            if (!setters.isEmpty()) {
                final String className = ((TypeElement) setters.get(0).getEnclosingElement()).getQualifiedName().toString();

                final Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
                        setter -> setter.getSimpleName().toString(),
                        setter -> ((ExecutableType) setter.asType()).getParameterTypes().get(0).toString()
                ));

                // ~ 生成文件
                try {
                    this.writeBuilderFile(className, setterMap);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        return true;
    }

    private void writeBuilderFile(String className, Map<String, String> setterMap) throws IOException {
        String packageName = null;
        final int indexOfLastDot = StringUtils.lastIndexOf(className, ".");
        if (indexOfLastDot > 0) {
            packageName = className.substring(0, indexOfLastDot);
        }

        final String simpleClassName = className.substring(indexOfLastDot + 1);
        final String builderClassName = className + "Builder";
        final String builderSimpleClassName = builderClassName.substring(indexOfLastDot + 1);

        final Filer filer = processingEnv.getFiler();
        final JavaFileObject builderFile = filer.createSourceFile(builderClassName);

        try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
            if (StringUtils.isNotBlank(packageName)) {
                out.print("package ");
                out.print(packageName);
                out.println(";");
                out.println();
            }

            out.print("public class ");
            out.print(builderSimpleClassName);
            out.println(" {");
            out.println();

            out.print("    private ");
            out.print(simpleClassName);
            out.print(" object = new ");
            out.print(simpleClassName);
            out.println("();");
            out.println();

            out.print("    public ");
            out.print(simpleClassName);
            out.println(" build() {");
            out.println("        return object;");
            out.println("    }");
            out.println();

            setterMap.forEach((methodName, argumentType) -> {
                out.print("    public ");
                out.print(builderSimpleClassName);
                out.print(" ");
                out.print(methodName);

                out.print("(");

                out.print(argumentType);
                out.println(" value) {");
                out.print("        object.");
                out.print(methodName);
                out.println("(value);");
                out.println("        return this;");
                out.println("    }");
                out.println();
            });

            out.println("}");
        }
    }
}
